//The MIT License
//
//Copyright (c) 2009 nodchip
//
//Permission is hereby granted, free of charge, to any person obtaining a copy
//of this software and associated documentation files (the "Software"), to deal
//in the Software without restriction, including without limitation the rights
//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//copies of the Software, and to permit persons to whom the Software is
//furnished to do so, subject to the following conditions:
//
//The above copyright notice and this permission notice shall be included in
//all copies or substantial portions of the Software.
//
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//THE SOFTWARE.
package tv.dyndns.kishibe.server;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import tv.dyndns.kishibe.client.packet.PacketRankingData;
import tv.dyndns.kishibe.server.database.Database;
import tv.dyndns.kishibe.server.database.DatabaseException;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.base.Objects;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.Multiset;
import com.google.common.collect.Sets;
import com.google.common.io.Files;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;

public class BadUserManager {
	private static final Logger logger = Logger.getLogger(BadUserManager.class.getName());
	private final Database database;
	private final ThreadPool threadPool;

	@Inject
	public BadUserManager(Database database, ThreadPool threadPool) {
		this.database = database;
		this.threadPool = threadPool;

		threadPool.addDailyTask(new Runnable() {
			@Override
			public void run() {
				try {
					updateCheater();
				} catch (IOException | DatabaseException e) {
					logger.log(Level.WARNING, "チートユーザーのアップデートに失敗しました", e);
				}
			}
		});
	}

	public void addIgnoreUserCode(final int userCode, final int targetUserCode) {
		threadPool.execute(new Runnable() {
			public void run() {
				try {
					database.addIgnoreUserCode(userCode, targetUserCode);
				} catch (DatabaseException e) {
					logger.log(Level.WARNING, "無視ユーザーの追加に失敗しました", e);
				}
			}
		});
	}

	public void removeIgnoreUserCode(final int userCode, final int targetUserCode) {
		threadPool.execute(new Runnable() {
			public void run() {
				try {
					database.removeIgnoreUserCode(userCode, targetUserCode);
				} catch (DatabaseException e) {
					logger.log(Level.WARNING, "無視ユーザーの削除に失敗しました", e);
				}
			}
		});
	}

	public boolean isLimitedUser(int userCode, String remoteAddress) {
		try {
			return database.getLimitedUserCodes().contains(userCode)
					|| database.getLimitedRemoteAddresses().contains(remoteAddress);
		} catch (DatabaseException e) {
			logger.log(Level.WARNING, "制限ユーザーの確認に失敗しました", e);
			return false;
		}
	}

	public void addLimitedUser(final int userCode, final String remoteAddress) {
		threadPool.execute(new Runnable() {
			@Override
			public void run() {
				try {
					database.addLimitedUserCode(userCode);
				} catch (DatabaseException e) {
					logger.log(Level.WARNING, "制限ユーザーのユーザーコードの追加に失敗しました", e);
				}

				if (!remoteAddress.equals("127.0.0.1")) {
					try {
						database.addLimitedRemoteAddress(remoteAddress);
					} catch (DatabaseException e) {
						logger.log(Level.WARNING, "制限ユーザーのIPアドレスの追加に失敗しました", e);
					}
				}
			}
		});
	}

	private static final FilenameFilter LOG_FILENAME_FILTER = new FilenameFilter() {
		@Override
		public boolean accept(File dir, String name) {
			return name.startsWith("catalina.") && name.endsWith(".log");
		}
	};
	private static final Pattern GAME_START_PATTERN = Pattern
			.compile("\\{method=transitFromMachingToReady, sessionId=([0-9]+?), userCode=([0-9]+?)\\}");
	private static final Pattern GAME_FINISHED_PATTERN = Pattern
			.compile("\\{method=notifyGameFinished, userCode=([0-9]+?), sessionId=([0-9]+?),");

	@VisibleForTesting
	static class SessionIdAndUserCode {
		private final int sessionId;
		private final int userCode;

		public SessionIdAndUserCode(int sessionId, int userCode) {
			this.sessionId = sessionId;
			this.userCode = userCode;
		}

		@Override
		public int hashCode() {
			return Objects.hashCode(sessionId, userCode);
		}

		@Override
		public boolean equals(Object obj) {
			if (!(obj instanceof SessionIdAndUserCode)) {
				return false;
			}
			SessionIdAndUserCode rh = (SessionIdAndUserCode) obj;
			return Objects.equal(sessionId, rh.sessionId) && Objects.equal(userCode, rh.userCode);
		}

		@Override
		public String toString() {
			return Objects.toStringHelper(this).add("sessionId", sessionId)
					.add("userCode", userCode).toString();
		}
	}

	private void updateCheater() throws IOException, DatabaseException {
		Set<SessionIdAndUserCode> startLogs = Sets.newHashSet();
		Set<SessionIdAndUserCode> finishLogs = Sets.newHashSet();

		// ゲーム開始回数とゲーム終了回数をカウントする
		// ゲーム開始カウントは2人以上でゲームを開始した場合
		// ゲーム終了カウントは常時
		for (File file : new File("/var/log/tomcat6").listFiles(LOG_FILENAME_FILTER)) {
			String body = Files.toString(file, Charsets.UTF_8);
			extractStartAndFinish(body, startLogs, finishLogs);
		}

		// ユーザーコード
		Multiset<Integer> startCounts = HashMultiset.create();
		Multiset<Integer> finishCounts = HashMultiset.create();
		for (SessionIdAndUserCode log : startLogs) {
			startCounts.add(log.userCode);
			if (finishLogs.contains(log)) {
				finishCounts.add(log.userCode);
			}
		}

		for (PacketRankingData user : database.getRankingData().get(3)) {
			int userCode = user.userCode;
			int startCount = startCounts.count(userCode);
			int finishCount = finishCounts.count(userCode);
			if (startCount < 3) {
				continue;
			}

			if (startCount <= finishCount * 2) {
				continue;
			}

			database.addLimitedUserCode(userCode);
			logger.log(Level.INFO, "制限ユーザーに追加しました: " + user + " startCount=" + startCount
					+ " finishCount=" + finishCount);
		}
	}

	@VisibleForTesting
	void extractStartAndFinish(String body, Set<SessionIdAndUserCode> startLogs,
			Set<SessionIdAndUserCode> finishLogs) {
		Matcher gameStartMatcher = GAME_START_PATTERN.matcher(body);
		while (gameStartMatcher.find()) {
			int sessionId = Integer.valueOf(gameStartMatcher.group(1));
			int userCode = Integer.valueOf(gameStartMatcher.group(2));
			startLogs.add(new SessionIdAndUserCode(sessionId, userCode));
		}

		Matcher gameFinishedMatcher = GAME_FINISHED_PATTERN.matcher(body);
		while (gameFinishedMatcher.find()) {
			int userCode = Integer.valueOf(gameFinishedMatcher.group(1));
			int sessionId = Integer.valueOf(gameFinishedMatcher.group(2));
			finishLogs.add(new SessionIdAndUserCode(sessionId, userCode));
		}
	}

	public static void main(String[] args) throws IOException, DatabaseException {
		Injector injector = Guice.createInjector(new QMACloneModule());
		BadUserManager badUserManager = injector.getInstance(BadUserManager.class);
		badUserManager.updateCheater();
		badUserManager.threadPool.shutdown();
	}
}
